package org.codefilarete.stalactite.engine.configurer.dslresolver;

import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import javax.annotation.Nullable;

import org.codefilarete.reflection.AccessorDefinition;
import org.codefilarete.reflection.MethodReferences;
import org.codefilarete.reflection.SerializablePropertyMutator;
import org.codefilarete.stalactite.dsl.MappingConfigurationException;
import org.codefilarete.stalactite.dsl.entity.EntityMappingConfiguration;
import org.codefilarete.stalactite.dsl.entity.FluentEntityMappingBuilder;
import org.codefilarete.stalactite.dsl.idpolicy.IdentifierPolicy;
import org.codefilarete.stalactite.dsl.key.FluentEntityMappingBuilderKeyOptions;
import org.codefilarete.stalactite.dsl.naming.AssociationTableNamingStrategy;
import org.codefilarete.stalactite.dsl.naming.ColumnNamingStrategy;
import org.codefilarete.stalactite.dsl.naming.ElementCollectionTableNamingStrategy;
import org.codefilarete.stalactite.dsl.naming.ForeignKeyNamingStrategy;
import org.codefilarete.stalactite.dsl.naming.JoinColumnNamingStrategy;
import org.codefilarete.stalactite.dsl.naming.MapEntryTableNamingStrategy;
import org.codefilarete.stalactite.dsl.naming.TableNamingStrategy;
import org.codefilarete.stalactite.dsl.naming.UniqueConstraintNamingStrategy;
import org.codefilarete.stalactite.engine.configurer.NamingConfiguration;
import org.codefilarete.stalactite.engine.configurer.model.IdentifierMapping;
import org.codefilarete.stalactite.sql.ddl.structure.Table;
import org.codefilarete.tool.Reflections;
import org.codefilarete.tool.VisibleForTesting;
import org.codefilarete.tool.collection.Iterables;
import org.codefilarete.tool.collection.KeepOrderSet;
import org.codefilarete.tool.function.SerializableTriFunction;

import static org.codefilarete.stalactite.dsl.entity.EntityMappingConfiguration.KeyMapping;
import static org.codefilarete.tool.Nullable.nullable;

/**
 * A class that collects the configurations inherited by a {@link EntityMappingConfiguration} inheritance to build
 * a {@link ResolvedConfiguration} for each configuration in the hierarchy.
 * The {@link ResolvedConfiguration}s are some ready-to-use instances that contain all the necessary information to build
 * an entity, without the need to go through the whole hierarchy again.
 * 
 * @param <C> the initial configuration entity type
 * @param <I> the configurations identifier type
 * @author Guillaume Mary
 */
public class InheritanceConfigurationResolver<C, I> {
	
	@VisibleForTesting
	static final SerializableTriFunction<FluentEntityMappingBuilder, SerializablePropertyMutator<?, ?>, IdentifierPolicy, FluentEntityMappingBuilderKeyOptions<?, ?>> IDENTIFIER_METHOD_REFERENCE = FluentEntityMappingBuilder::mapKey;
	
	/**
	 * Returns a {@link ResolvedConfiguration} for each configuration in the given configuration hierarchy
	 * 
	 * @param mappingConfiguration the configuration to start from
	 * @return a {@link ResolvedConfiguration} for each configuration in the hierarchy

	 */
	KeepOrderSet<ResolvedConfiguration<?, I>> resolveConfigurations(EntityMappingConfiguration<C, I> mappingConfiguration) {
		NamingConfiguration defaultNamingConfiguration = new NamingConfiguration(
				TableNamingStrategy.DEFAULT,
				ColumnNamingStrategy.DEFAULT,
				ForeignKeyNamingStrategy.DEFAULT,
				UniqueConstraintNamingStrategy.DEFAULT,
				ElementCollectionTableNamingStrategy.DEFAULT,
				MapEntryTableNamingStrategy.DEFAULT,
				JoinColumnNamingStrategy.JOIN_DEFAULT,
				ColumnNamingStrategy.INDEX_DEFAULT,
				AssociationTableNamingStrategy.DEFAULT);
		
		NamingConfiguration inheritedNaming = defaultNamingConfiguration;
		KeyMapping<?, I> inheritedKey;
		EntityMappingConfiguration<?, ?> keyDefiner = null;
		
		List<EntityMappingConfiguration<?, I>> bottomToTopConfigurations = Iterables.asList(mappingConfiguration.inheritanceIterable());
		Iterable<EntityMappingConfiguration<?, I>> topToBottomConfigurations = () -> Iterables.reverseIterator(bottomToTopConfigurations);
		
		// Using the same top-to-bottom loop to determine the naming strategies and the identifier because those
		// data follow the same and usual inheritance principle: the ancestor applies if none is defined locally
		KeepOrderSet<ResolvedConfiguration<?, I>> topToBottomResult = new KeepOrderSet<>();
		for (EntityMappingConfiguration<?, I> node : topToBottomConfigurations) {
			ResolvedConfiguration<?, I> nodeConfiguration = new ResolvedConfiguration<>(node);
			topToBottomResult.add(nodeConfiguration);
			
			// 1. Computing naming conventions: it follows usual inheritance rules, the highest configuration declares default
			// behaviors, which can be overridden by sub-configurations
			NamingConfiguration nodeNamingConventions = new NamingConfiguration(
					nullable(node.getTableNamingStrategy()).getOr(inheritedNaming.getTableNamingStrategy()),
					nullable(node.getColumnNamingStrategy()).getOr(inheritedNaming.getColumnNamingStrategy()),
					nullable(node.getForeignKeyNamingStrategy()).getOr(inheritedNaming.getForeignKeyNamingStrategy()),
					nullable(node.getUniqueConstraintNamingStrategy()).getOr(inheritedNaming.getUniqueConstraintNamingStrategy()),
					nullable(node.getElementCollectionTableNamingStrategy()).getOr(inheritedNaming.getElementCollectionTableNamingStrategy()),
					nullable(node.getEntryMapTableNamingStrategy()).getOr(inheritedNaming.getEntryMapTableNamingStrategy()),
					nullable(node.getJoinColumnNamingStrategy()).getOr(inheritedNaming.getJoinColumnNamingStrategy()),
					nullable(node.getIndexColumnNamingStrategy()).getOr(inheritedNaming.getIndexColumnNamingStrategy()),
					nullable(node.getAssociationTableNamingStrategy()).getOr(inheritedNaming.getAssociationTableNamingStrategy()));
			
			nodeConfiguration.setNamingConfiguration(nodeNamingConventions);
			inheritedNaming = nodeNamingConventions;
			
			// 2. Key mapping: it follows the inheritance rules:
			// - null for ancestors before definer,
			// - null for descendants
			// - thus, can't be overridden by sub-configurations: an exception is thrown where it's redefined
			if (node.getKeyMapping() != null) {
				if (keyDefiner != null) {
					// only one definer is allowed in the full chain
					throw new MappingConfigurationException("Identifier policy is defined twice in the hierarchy : first by "
							+ AccessorDefinition.toString(keyDefiner.getKeyMapping().getAccessor())
							+ ", then by " + AccessorDefinition.toString(node.getKeyMapping().getAccessor()));
				}
				inheritedKey = node.getKeyMapping();
				keyDefiner = node;
				nodeConfiguration.setKeyMapping(inheritedKey);
			}
		}
		
		if (keyDefiner == null) {
			throw newMissingIdentificationException(mappingConfiguration.getEntityType());
		}
		
		Map<EntityMappingConfiguration<?, I>, ResolvedConfiguration<?, I>> configurationPerMapping = Iterables.map(topToBottomResult, ResolvedConfiguration::getMappingConfiguration, () -> new HashMap<>());
		// 3. deducing Tables from configuration hierarchy
		// - it's the opposite of usual inheritance rules: the lowest configuration defines the table, which is shared with ancestors if joining tables is not defined
		// - when joining tables flag is set, a new table is created for parent configuration
		// Note that this step depend on naming strategy for Table creation, hence it must be done after naming strategy resolution
		Table currentSegmentTable = takeConfigurationTableOrCreateOne(mappingConfiguration, configurationPerMapping);
		EntityMappingConfiguration<?, I> pawn = mappingConfiguration;
		while(pawn.getInheritanceConfiguration() != null) {
			configurationPerMapping.get(pawn).setTable(currentSegmentTable);
			if (pawn.getInheritanceConfiguration().isJoiningTables()) {
				EntityMappingConfiguration<?, I> parentMappingConfiguration = pawn.getInheritanceConfiguration().getParentMappingConfiguration();
				Table joiningTable = takeConfigurationTableOrCreateOne(parentMappingConfiguration, configurationPerMapping);
				currentSegmentTable = joiningTable;
			}
			pawn = pawn.getInheritanceConfiguration().getParentMappingConfiguration();
		}
		// handling the latest configuration because algorithm above didn't iterate over it
		configurationPerMapping.get(pawn).setTable(currentSegmentTable);
		
		List<ResolvedConfiguration<?, I>> result = new ArrayList<>(topToBottomResult.getDelegate());
		Collections.reverse(result);
		return new KeepOrderSet<>(result);
	}
	
	protected UnsupportedOperationException newMissingIdentificationException(Class<C> entityType) {
		return new UnsupportedOperationException("Identifier is not defined for " + Reflections.toString(entityType)
				+ ", please add one through " + MethodReferences.toMethodReferenceString(IDENTIFIER_METHOD_REFERENCE) + " variants");
	}
	
	@Nullable
	private <X, Y> Table takeConfigurationTableOrCreateOne(EntityMappingConfiguration<X, Y> mappingConfiguration, Map<EntityMappingConfiguration<?, Y>, ResolvedConfiguration<?, Y>> result) {
		return nullable(mappingConfiguration.getTable())
				.elseSet(() -> new Table(result.get(mappingConfiguration).getNamingConfiguration()
						.getTableNamingStrategy()
						.giveName(mappingConfiguration.getEntityType())))
				.get();
	}
	
	public static class ResolvedConfiguration<C, I> {
		
		private final EntityMappingConfiguration<C, I> mappingConfiguration;
		
		private Table table;
		
		private NamingConfiguration namingConfiguration;
		
		/**
		 * Null for ancestors before definer, inherited for descendants
		 */
		@Nullable
		private KeyMapping<C, I> keyMapping;
		
		@Nullable
		private IdentifierMapping<C, I> identifierMapping;
		
		ResolvedConfiguration(EntityMappingConfiguration<C, I> node) {
			this.mappingConfiguration = node;
		}
		
		public EntityMappingConfiguration<C, I> getMappingConfiguration() {
			return mappingConfiguration;
		}
		
		public Table getTable() {
			return table;
		}
		
		public void setTable(Table table) {
			this.table = table;
		}
		
		public NamingConfiguration getNamingConfiguration() {
			return namingConfiguration;
		}
		
		public void setNamingConfiguration(NamingConfiguration namingConfiguration) {
			this.namingConfiguration = namingConfiguration;
		}
		
		@Nullable
		public KeyMapping getKeyMapping() {
			return keyMapping;
		}
		
		public void setKeyMapping(@Nullable KeyMapping keyMapping) {
			this.keyMapping = keyMapping;
		}
		
		@Nullable
		public IdentifierMapping getIdentifierMapping() {
			return identifierMapping;
		}
		
		public void setIdentifierMapping(@Nullable IdentifierMapping identifierMapping) {
			this.identifierMapping = identifierMapping;
		}
	}
}
